<?php
/*
Plugin Name: Post Similarity
Plugin URI: http://kaloyan.info/blog/wp-post-similarty-plugin/
Description: This plugin is designed to offer you a feature which returns all entries to a post based on keyword matches.
Author: Kaloyan K. Tsvetkov
Version: 0.1
Author URI: http://kaloyan.info/
*/

/////////////////////////////////////////////////////////////////////////////

/**
* @internal prevent from direct calls
*/
if (!defined('ABSPATH')) {
	return ;
	}

/**
* @internal prevent from second inclusion
*/
if (!class_exists('wp_post_similarity')) {

/////////////////////////////////////////////////////////////////////////////

/**
* "Post Similarity" WordPress Plugin
*
* @author Kaloyan K. Tsvetkov <kaloyan@kaloyan.info>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/
Class wp_post_similarity {

	// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 

	/**
	* Constructor
	*
	* This constructor attaches the needer plugin hook callbacks
	*/
	Function wp_post_similarity() {

		// modify the content
		//
		add_action('the_content', array($this, 'the_content'));

		// attach to admin menu
		//
		if (is_admin()) {
			add_action('admin_menu',
				array(&$this, '_menu')
				);
			}

		// attach to plugin installation
		//
		add_action(
			'activate_' . str_replace(
				DIRECTORY_SEPARATOR, '/',
				str_replace(
					realpath(ABSPATH . PLUGINDIR) . DIRECTORY_SEPARATOR,
						'', __FILE__
					)
				),
			array(&$this, 'install')
			);
		}

	// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
	
	/**
	* Append the similar posts to the content
	* @param string $content
	* @return string
	*/
	Function the_content($content) {
		
		// is append allowed ?
		//
		static $_;
		if (!isset($_)) {
			$wp_post_similarity_settings =
				(array) get_option('wp_post_similarity_settings');
			$_ = $wp_post_similarity_settings['append'];
			}
		
		return $content . ($_ ? $this->similar_posts() : '');
		}
	
	// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
	
	/**
	* Render the similar posts
	* @param string|array $query
	* @return string
	*/
	Function similar_posts($query='') {
		
		global $wpdb, $post;
		
		// parse query ?
		//
		is_array($query)
			? ($_ = $query)
			: parse_str($query, $_);

		// mix w\ default ones
		//
		$wp_post_similarity_settings = array_merge(
			(array) get_option('wp_post_similarity_settings'),
			$_
			);
		
		// nothing to render!
		//
		if (!$wp_post_similarity_settings['content_limit']) {
			return '';
			}
		
		// keywords ?
		//
		$keywords = ($custom = get_post_custom_values('post_similarity'))
			? $custom
			: $post->post_title;
		$keywords = preg_replace('~(&#\d+;)|([^a-z0-9_-]+)~Uis' ,' ', $keywords);
		$keywords = trim(strToLower($keywords));
		
		// current timestamp ?
		//
		$now = time() + get_option('gmt_offset')*3600;
		
		// compose query
		//
		$sql = "SELECT ID, post_title, post_excerpt, Match(post_title, post_content) Against ('" .$wpdb->escape($keywords). "') as `score`
			FROM {$wpdb->posts}
			WHERE Match(post_title, post_content) Against ('" .$wpdb->escape($keywords). "')
				AND post_status IN ('publish')
				AND ID != '{$post->ID}'
				AND post_date <= From_Unixtime('{$now}')
			ORDER BY score DESC
			LIMIT 0, 0{$wp_post_similarity_settings['content_limit']} ";
		
		// no results ?
		//
		if (!$results = $wpdb->get_results($sql)) {
			return $wp_post_similarity_settings['list_before']
				. $wp_post_similarity_settings['content_before']
				. $wp_post_similarity_settings['nothing']
				. $wp_post_similarity_settings['content_after']
				. $wp_post_similarity_settings['list_after'];
			}

		// render the results
		//
		$output = $wp_post_similarity_settings['list_before'];
		foreach($results as $r) {
			
			// cook the details
			//
			$title = apply_filters('the_title', stripslashes($r->post_title));
			
			// render
			//
			$output .= $wp_post_similarity_settings['content_before'];
			$output .= '<a href="' . get_permalink($r->ID)
					. '" rel="bookmark" title="Permanent Link: '
					. $title . '">' . $title . '</a>';
			
			// excerpt ?
			//
			if ($wp_post_similarity_settings['excerpt']) {
				
				$output .= $wp_post_similarity_settings['excerpt_before'];
				$output .= ($words = abs(intval($wp_post_similarity_settings['excerpt_words'])))
					? preg_replace(
						'~^\s*((?:\S+\s+){0,' . $words . '}).*$~',
						'\\1&hellip;',
						strip_tags($r->post_excerpt)
						)
					: $r->post_excerpt;
				$output .= $wp_post_similarity_settings['excerpt_after'];
				}
			
			$output .= $wp_post_similarity_settings['content_after'];
			}
		$output .= $wp_post_similarity_settings['list_after'];
		
		return $output;
		}
	
	// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 

	/**
	* Performs the routines required at plugin installation: 
	* in general introducing the settings array
	*/	
	Function install() {

		// sql
		//
		global $wpdb;
		$_ = $wpdb->get_var("SHOW CREATE TABLE `{$wpdb->posts}`");
		if (!preg_match('~posts_simillar~Uis', $_)) {
			$wpdb->query(
				"ALTER TABLE `{$wpdb->posts}`
					ADD FULLTEXT `posts_simillar`
						( `post_title` , `post_content`); "
				);
			}

		// options
		//
		$default = array(
			'content_limit' => 3,

			'list_before' => '<h4>Similar Posts</h4><ul>',
			'list_after' => '</ul>',
			
			'content_before' => '<li>',
			'content_after' => '</li>',
			
			'nothing' => 'No similar posts',
			
			'append' => 0,
			
			'excerpt' => 0,
			'excerpt_words' => 25,
			'excerpt_before' => '<br/>',
			'excerpt_after' => '<br/>',
					
			);

		add_option(
			'wp_post_similarity_settings',
			$default
			);
		
		update_option(
			'wp_post_similarity_settings',
			array_merge(
				$default,
				(array) get_option('wp_post_similarity_settings')
				)
			);
		}

	// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
	
	/**
	* Attach the menu page to the `Options` tab
	*/
	Function _menu() {
		add_submenu_page('options-general.php',
			 'Post Similarity',
			 'Post Similarity', 8,
			 __FILE__,
			 array($this, 'menu')
			);
		}
		
	/**
	* Handles and renders the menu page
	*/
	Function menu() {

		// sanitize referrer
		//
		$_SERVER['HTTP_REFERER'] = preg_replace(
			'~&saved=.*$~Uis','', $_SERVER['HTTP_REFERER']
			);
		
		// information updated ?
		//
		if ($_POST['submit']) {
			
			$_POST['wp_post_similarity_settings'] = array_map(
				create_function('$v', 'return stripSlashes($v);'),
				(array) $_POST['wp_post_similarity_settings']
				);
			
			// save
			//
			update_option(
				'wp_post_similarity_settings',
				$_POST['wp_post_similarity_settings']
				);

			die("<script>document.location.href = '{$_SERVER['HTTP_REFERER']}&saved=settings:" . time() . "';</script>");
			}

		// operation report detected
		//
		if (@$_GET['saved']) {
			
			list($saved, $ts) = explode(':', $_GET['saved']);
			if (time() - $ts < 10) {
				echo '<div class="updated"><p>';
	
				switch ($saved) {
					case 'settings' :
						echo 'Settings saved.';
						break;
					}
	
				echo '</p></div>';
				}
			}

		// read the settings
		//
		$wp_post_similarity_settings = (array) get_option('wp_post_similarity_settings');

?>
<div class="wrap">
	<h2>Post Similarity Options</h2>
	<p>For more information please visit the <a href="http://kaloyan.info/blog/wp-post-similarty-plugin/">Post Similarity</a> homepage.</p>
	<form method="post">

	<p class="submit"><input type="submit" name="submit" value="Update Options &raquo;" /></p>
	
	<fieldset class="options">
		<legend>General Settings</legend>
	
		<table class="optiontable" border="0">
	
			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_content_limit">Number of similar posts to show:</label><br/>
			</th><td>
			<select id="wp_post_similarity_settings_content_limit"
				name="wp_post_similarity_settings[content_limit]">
<?php
			$_ = array(0, 1,2,3,4,5,6,7,8,9,10);
			foreach ($_ as $p) {
				echo '<option value="', $p ,'" ',
					($wp_post_similarity_settings['content_limit'] == $p ? ' selected="selected" > &raquo; ' : '>'),
					$p , ' &nbsp; </option>';
				}
?>
			</select>
			<br/>To hide the list of similar posts, set the number to show to 0.
			</td></tr>

			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_list_before">HTML to show before posts list:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_list_before" size="56"
				name="wp_post_similarity_settings[list_before]"
					value="<?php echo $wp_post_similarity_settings['list_before'] ?>"/>
			<br/>This is the HTML code that will appear before the similar posts' list.
			</td></tr>
			
			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_list_after">HTML to show after posts list:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_list_after" size="56"
				name="wp_post_similarity_settings[list_after]"
					value="<?php echo $wp_post_similarity_settings['list_after'] ?>"/>
			<br/>This is the HTML code that will appear after similar posts' list.
			</td></tr>
		
			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_content_before">HTML to show before post title:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_content_before" size="56"
				name="wp_post_similarity_settings[content_before]"
					value="<?php echo $wp_post_similarity_settings['content_before'] ?>"/>
			<br/>This is the HTML code that will appear before each title.
			</td></tr>
			
			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_content_after">HTML to show after post title:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_content_after" size="56"
				name="wp_post_similarity_settings[content_after]"
					value="<?php echo $wp_post_similarity_settings['content_after'] ?>"/>
			<br/>This is the HTML code that will appear after each title.
			</td></tr>

			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_nothing"><b>Nothing Found</b> caption:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_nothing" size="56"
				name="wp_post_similarity_settings[nothing]"
					value="<?php echo $wp_post_similarity_settings['nothing'] ?>"/>
			<br/>This caption will appear if no similar posts are found.
			<br/>Leave it empty if you want to show nothing.
			</td></tr>

			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_append">Automatically Append to Posts:</label><br/>
			</th><td>
			
			<select id="wp_post_similarity_settings_append" name="wp_post_similarity_settings[append]">
<?php
			$_ = array(0=>'No',1=>'Yes');
			foreach ($_ as $k=>$v) {
				echo '<option value="', $k ,'" ',
					($wp_post_similarity_settings['append'] == $k ? ' selected="selected" > &raquo; ' : '>'),
					$v , ' &nbsp; </option>';
				}
?>
			</select>
			<br/>Use this option if you want to have the similar posts automatically<br/>
			appended to the posts. Otherwise you have to put the tag that renders the<br/>
			similar posts yourself inside the templates of the theme you are using.
			</td></tr>

		</table>
		
	</fieldset>
	<fieldset class="options">
		
		<legend>Excerpts Settings</legend>
		
		<table class="optiontable" border="0">
	
			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_excerpt">Show Excerpts:</label><br/>
			</th><td>
			
			<select id="wp_post_similarity_settings_excerpt" name="wp_post_similarity_settings[excerpt]">
<?php
			$_ = array(0=>'No',1=>'Yes');
			foreach ($_ as $k=>$v) {
				echo '<option value="', $k ,'" ',
					($wp_post_similarity_settings['excerpt'] == $k ? ' selected="selected" > &raquo; ' : '>'),
					$v , ' &nbsp; </option>';
				}
?>
			</select>
			<br/>Whether to show the excerpt after the posts' titles.
			</td></tr>

			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_excerpt_words">Number of words in excerpts:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_excerpt_words" style="text-align:center;" size="10"
				name="wp_post_similarity_settings[excerpt_words]"
					value="<?php echo abs(intval($wp_post_similarity_settings['excerpt_words'])) ?>"/>
			<br/>Restrict length of the excerpt (set to 0 to show full excerpt).
			</td></tr>

			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_excerpt_before">HTML to show before excerpt:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_excerpt_before" size="56"
				name="wp_post_similarity_settings[excerpt_before]"
					value="<?php echo $wp_post_similarity_settings['excerpt_before'] ?>"/>
			<br/>This is the HTML code that will appear before each excerpt.
			</td></tr>

			<tr valign="top"><th>
			<label for="wp_post_similarity_settings_excerpt_after">HTML to show after excerpt:</label><br/>
			</th><td>
			<input id="wp_post_similarity_settings_excerpt_after" size="56"
				name="wp_post_similarity_settings[excerpt_after]"
					value="<?php echo $wp_post_similarity_settings['excerpt_after'] ?>"/>
			<br/>This is the HTML code that will appear after each excerpt.
			</td></tr>

		</table>
		</blockquote>
	</fieldset>

	<p class="submit" ><input type="submit" name="submit" value="Update Options &raquo;" /></p>

	</form>
</div>

<div class="wrap">
	<h2>Criteria</h2>
	<p>
	This plugin is using the title and the content of your posts to 
	calculate the relevance criteria in order to pick up the posts that are 
	similar. However, there is a mechanism to override this default 
	behavior. If you want to control the keywords used for finding the 
	similar posts, you should add an extra custom field called 
	<b>"post_similarity"</b>. In it you should put all the keywords you want 
	to be used for choosing the related posts for the particular post you 
	are editing.
	</p>
	
	<p>For more information please visit the <a href="http://kaloyan.info/blog/wp-post-similarty-plugin/">Post Similarity</a> homepage.</p>
</div>

<div class="wrap">
<style>
.wrap pre {background: #C1E0FF; padding: 1em;}
</style>
	<h2>Usage</h2>
	<p>
	The easiest way to use this plugin, is to enable the <label 
	for="wp_post_similarity_settings_append"><b>Automatically Append to 
	Posts</b></label> option, which will automatically put the similar posts 
	after your posts. This will work regardless what WordPress theme you are 
	using.
	</p>

	<p>
	However, if you decide to put the similar posts list at some other place 
	on your posts page, you might use the <code>post_similarity()</code> 
	"tag" function (after <u>disabling</u> the <label 
	for="wp_post_similarity_settings_append"><b>Automatically Append to 
	Posts</b></label> option - otherwise you will see two similar posts 
	lists).
	</p>

	<p>
	Above in the <b>Options</b> form you see all the settings you can modify 
	regarding this plugin. However, if you deside to use the "tag" function, 
	you can override all the "default" values (set up by the form above), 
	with local values passed as argument to the 
	<code>post_similarity()</code> function. Here is an example:
	</p>

<pre>post_similarity('list_before=&list_after=');</pre>

	<p>
	If you are familiar with the WordPress tags, this format will be nothing 
	difficult for you. If you prefer passing the arguments as an array, it 
	is also supported; here is an example:
	</p>

<pre>post_similarity(array('list_before => '', 'list_after' => '');</pre>

	<p>
	Here are names of the options for when using them as "local" values:
	</p>

	<ul>
		<li><b>content_limit</b>: Number of similar posts to show</li>
		<li><b>list_before</b>: HTML to show before posts list</li>
		<li><b>list_after</b>: HTML to show after posts list</li>
		<li><b>content_before</b>: HTML to show before post title</li>
		<li><b>content_after</b>: HTML to show after post title</li>
		<li><b>nothing</b>: "Nothing Found" caption</li>
		<li><b>excerpt</b>: Show Excerpts</li>
		<li><b>excerpt_words</b>: Number of words in excerpts</li>
		<li><b>excerpt_before</b>: HTML to show before excerpt</li>
		<li><b>excerpt_after</b>: HTML to show after excerpt</li>
		</li>
	</ul>

	<p>
	<b>A word of advice</b>: placing the "tag" function inside your themes 
	might cause some inconveniences when you decide to turn off the "Post 
	Similarity" plugin. That's why it is best to <u>ALWAYS</u> use the "tag" 
	function like this:
	</p>

<pre>&lt;?php if (function_exists('post_similarity')) post_similarity('content_limit=3'); ?&gt;</pre>

	<p>
	In this way, if the plugin is disabled, and the 
	<code>post_similarity()</code> function is missing, you will not see an 
	error. This is something common for all WordPress plugins which offer 
	extra "tags", so if you want to save yourself the headaches, always use 
	this type of syntax for when you need to place a "tag" function inside 
	your templates.
	</p>

	<p>For more information please visit the <a href="http://kaloyan.info/blog/wp-post-similarty-plugin/">Post Similarity</a> homepage.</p>

</div>

<?php
		}
	
	// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
	
	//--end-of-class
	}

}

/////////////////////////////////////////////////////////////////////////////

/**
* Initiating the plugin...
* @see wp_post_similarity
*/
new wp_post_similarity;

/////////////////////////////////////////////////////////////////////////////

/**
* Declare a `tag` function for using it in the templates
* @param string|array $query
*/
function post_similarity($query='') {
	echo wp_post_similarity::similar_posts($query);
	}

/////////////////////////////////////////////////////////////////////////////

?>